Set、Sorted Set 与 GEO
系统梳理 Redis Set、Sorted Set 与 GEO 的使用场景、集合运算风险、排行榜、延迟任务、double score 精度、跳表实现和 Go 使用边界。
第 6 章:Set、Sorted Set 与 GEO
1. 本章定位
本章解决三类问题:
- Set:某个元素是否属于一个集合,以及多个集合之间有什么关系。
- Sorted Set:既要保证成员唯一,又要按照分值持续排序。
- GEO:存储经纬度,并完成距离计算和附近位置检索。
三者看似应用差异很大,底层却存在联系:Set 侧重唯一性和集合运算;Sorted Set 在唯一成员之上增加顺序;GEO 又建立在 Sorted Set 之上,将地理坐标编码到 score 中。
截至 2026 年 6 月,本章以 Redis Open Source 8.8 为技术基线,同时说明旧版本中常见的实现差异。(Redis)
2. 学习目标
学完本章,应当能够:
- 使用 Set 实现去重、标签、权限、共同关注和抽奖。
- 评估
SINTER、SUNION、SDIFF对 Redis 延迟的影响。 - 使用 Sorted Set 实现排行榜、权重队列、滑动时间窗口和延迟任务。
- 正确解释重复 score、成员唯一性和字典序规则。
- 理解 double score 的精度边界,避免时间戳和复合分值错误。
- 解释 Sorted Set 为什么同时使用字典和跳表。
- 说明 GEO 的存储方式、距离误差和适用边界。
- 使用
go-redis/v9编写具备超时、错误处理和并发安全意识的代码。
2.1 本章边界与跳转
本章是 Set、Sorted Set 与 GEO 的主章节。Sorted Set 延迟任务这里只讲数据结构建模和基础领取风险;生产级延迟任务的认领、确认、重试和幂等状态机见第 19 章,Streams 消费者组方案见[第 8 章](/blog/tech/Redis/08.PubSub、List 队列与 Streams/);跳表、dict、intset 等底层结构见[第 11 章](/blog/tech/Redis/11.Redis 核心对象与底层数据结构/);大集合运算造成的性能问题见[第 12 章](/blog/tech/Redis/12.Redis 高性能原理/)和第 20 章。
3. 核心概念
3.1 Set:无序且成员唯一
Redis Set 是一个无序的唯一字符串集合。同一成员重复执行 SADD 不会产生两份数据,命令返回值可以区分本次是否真的新增了成员。(Redis)
典型建模如下:
| 业务 | Key | Member |
|---|---|---|
| 用户关注列表 | follow:user:1001 | 被关注用户 ID |
| 文章标签关系 | article:88:tags | 标签 ID |
| 标签反向索引 | tag:redis:articles | 文章 ID |
| 角色成员 | role:admin:users | 用户 ID |
| 活动候选人 | lottery:2026:users | 用户 ID |
| 请求去重 | dedup:order:20260621 | 请求唯一 ID |
Set 的优势不只是去重,还包括三类集合运算:
- 交集:同时属于多个集合的成员,例如共同关注。
- 并集:至少属于一个集合的成员,例如合并多个人群。
- 差集:属于 A 但不属于 B 的成员,例如“我关注但你没有关注的人”。
3.2 Set 不保证业务顺序
Set 没有插入顺序、更新时间顺序或稳定返回顺序。即使某次 SMEMBERS 的结果看起来固定,也不能把这种偶然顺序写进业务协议。
需要顺序时应改用:
- List:按插入顺序或队列顺序。
- Sorted Set:按业务分值、时间或优先级排序。
- Stream:需要消息 ID、消费者组和确认机制。
3.3 大集合运算为什么危险
SINTER、SUNION、SDIFF 都需要访问大量成员。其风险不仅是算法复杂度,还包括:
- Redis 执行命令期间占用命令执行线程。
- 需要为结果分配内存。
- 结果要编码成 RESP 响应。
- 大结果通过网络发送,占用带宽和客户端输出缓冲区。
- 客户端还需要分配内存反序列化。
官方复杂度中,SINTER 最坏可达到 O(N × M),其中 N 是最小集合的基数,M 是参与运算的集合数;SUNION、SDIFF 与输入集合成员总量相关。(Redis)
因此,“共同关注只有十几个结果”并不意味着运算便宜。Redis 仍可能扫描一个拥有数百万成员的集合。
3.4 Sorted Set:唯一成员加可排序 score
Sorted Set,简称 ZSet,由两部分组成:
- member:唯一字符串。
- score:用于排序的 64 位 double 浮点数。
同一 member 再次执行 ZADD,默认会更新其 score,而不是创建重复成员。不同 member 可以拥有相同 score。
默认排序规则为:
- score 从小到大。
- score 相同时,按 member 的二进制字节序排序。
- 使用反向范围时,score 从大到小,相同 score 按反向字典序排序。(Redis)
这意味着“积分相同就按先到先得”不会自然成立。要实现确定的第二排序字段,必须显式设计。
3.5 Sorted Set 的四类核心场景
1. 排行榜
key = leaderboard:game:2026-06
member = userID
score = 用户积分
使用 ZINCRBY 原子增加积分,用 ZRANGE ... REV 查询高分用户,用 ZREVRANK 查询用户从高到低的名次。
2. 权重队列
key = queue:image:priority
member = taskID
score = 优先级
如果较小 score 代表更高优先级,可用 ZPOPMIN;如果较大 score 优先,则使用 ZPOPMAX 或转换评分方向。
权重队列还要处理饥饿问题:高优先级任务持续进入时,低优先级任务可能长期得不到处理。常见改进是引入等待时间加权或分级队列。
3. 时间窗口
key = request:user:1001
member = 唯一请求 ID
score = 请求发生时间戳
通过 score 范围查询最近一分钟的请求,并定期删除窗口之外的数据。这适用于精确滑动日志限流,但高频请求下内存成本较高。
4. 延迟调度
key = delay:{email}:ready
member = taskID
score = 计划执行时间
消费者查找 score <= 当前时间 的任务。但“查找后删除”是两个步骤,多消费者并发时会产生竞态,必须通过 Lua、事务或其他原子领取机制修正。
3.6 score 精度边界
Sorted Set 的 score 是 64 位 double。整数只有在 [-2^53, 2^53] 范围内才能被精确表示,即大约正负 9.007 × 10^15。超过这一范围后,相邻整数可能映射为同一个浮点值。(Redis)
工程上应注意:
- 当前 Unix 毫秒时间戳约为
10^12,可以精确表示。 - Unix 微秒时间戳约为
10^15,仍在边界内,但剩余空间有限。 - Unix 纳秒时间戳约为
10^18,不能作为精确整数 score。 - 不要随意通过“时间戳加一个极小小数”实现第二排序字段。
- 分数、时间、权重最好统一成有明确量纲的整数,再转换为
float64。 - 若必须构造复合 score,应证明最大值始终小于
2^53。
3.7 GEO:建立在 Sorted Set 上的地理索引
Redis GEO 用于:
- 存储经纬度。
- 计算两个成员之间的直线距离。
- 查询给定圆形或矩形范围内的成员。
它适合“附近门店”“附近车辆”“附近骑行站点”等简单场景,但不是完整 GIS 系统,不擅长:
- 多边形包含判断。
- 道路导航距离。
- 海拔和三维空间。
- 行政区边界。
- 复杂地理条件组合。
- 极高精度测绘。
官方也明确区分了基础 GEO 数据类型与 Redis Search 中更丰富的地理检索能力。(Redis)
4. 命令与 Go 使用方法
4.1 Set 常用命令
# 添加关注,重复成员不会重复存储
SADD follow:user:1001 user:2001 user:2002 user:2003
# 取消关注
SREM follow:user:1001 user:2002
# 判断是否关注
SISMEMBER follow:user:1001 user:2001
# 共同关注
SINTER follow:user:1001 follow:user:1002
# 两人的全部关注对象
SUNION follow:user:1001 follow:user:1002
# user:1001 关注、但 user:1002 未关注的人
SDIFF follow:user:1001 follow:user:1002
# 随机抽取但不删除
SRANDMEMBER lottery:2026:users 10
# 随机抽取并删除,适用于不重复中奖
SPOP lottery:2026:users 10
SPOP 只是从 Redis 集合中删除成员,并不能单独构成完整抽奖系统。真实抽奖还要处理资格快照、审计记录、并发开奖、结果持久化和失败重试。
4.2 Sorted Set 常用命令
# 添加或更新积分
ZADD leaderboard:game:2026-06 1500 user:1001
ZADD leaderboard:game:2026-06 1300 user:1002
# 原子增加积分
ZINCRBY leaderboard:game:2026-06 50 user:1001
# 从低分到高分查询
ZRANGE leaderboard:game:2026-06 0 9 WITHSCORES
# Redis 6.2+:从高分到低分查询
ZRANGE leaderboard:game:2026-06 0 9 REV WITHSCORES
# 从低到高的排名,排名从 0 开始
ZRANK leaderboard:game:2026-06 user:1001
# 从高到低的排名
ZREVRANK leaderboard:game:2026-06 user:1001
# 弹出最低分成员
ZPOPMIN queue:image:priority 1
# 查询当前已到执行时间的任务;仅查询,不是安全领取
ZRANGE delay:{email}:ready -inf 1782000000000 BYSCORE LIMIT 0 10
ZRANGE 的区间包含首尾位置;按排名查询的复杂度为 O(log N + M),其中 M 是返回数量。较大的 offset 仍可能产生明显遍历成本。(Redis)
4.3 GEO 常用命令
# 注意参数顺序是经度、纬度
GEOADD geo:shops 116.397128 39.916527 shop:1
GEOADD geo:shops 116.405285 39.904989 shop:2
# 计算两点距离
GEODIST geo:shops shop:1 shop:2 km
# Redis 6.2+:以经纬度为中心查询 5 公里内的位置
GEOSEARCH geo:shops \
FROMLONLAT 116.397128 39.916527 \
BYRADIUS 5 km \
ASC COUNT 20 WITHDIST
# GEO 实际存储为 Sorted Set,可使用 ZREM 删除
ZREM geo:shops shop:2
GEOSEARCH 支持按圆形或矩形查询。COUNT ... ANY 允许 Redis 找到足够结果后提前停止,可能更快,但不保证返回的就是全局最近成员。GEORADIUS 系列命令自 Redis 6.2 起已被标记为废弃方向,新代码优先使用 GEOSEARCH 和 GEOSEARCHSTORE。(Redis)
4.4 Go 客户端基础配置
package redisexample
import (
"context"
"errors"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
func NewRedisClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
DialTimeout: 2 * time.Second,
ReadTimeout: 800 * time.Millisecond,
WriteTimeout: 800 * time.Millisecond,
PoolTimeout: 1 * time.Second,
})
}
redis.Client 内部管理连接池,可以被多个 goroutine 并发复用。不要为每个请求创建一个 Client;应在进程级复用,并在进程退出时调用 Close。
网络读写超时是客户端级上限,每个业务调用仍应使用更具体的 context.WithTimeout。
4.5 Go 实现共同关注
func CommonFollows(
parent context.Context,
rdb *redis.Client,
userA, userB string,
) ([]string, error) {
ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
defer cancel()
keyA := "follow:" + userA
keyB := "follow:" + userB
users, err := rdb.SInter(ctx, keyA, keyB).Result()
if err != nil {
return nil, fmt.Errorf("SINTER %s %s: %w", keyA, keyB, err)
}
return users, nil
}
注意事项:
- 没有共同关注时通常返回空切片,不是
redis.Nil。 - 两个集合很大时,这段代码虽然简单,却可能形成慢命令。
- Redis Cluster 中两个 Key 必须位于同一 Slot 才能直接执行多 Key 集合运算。
- 不能仅通过缩短 Go 超时解决服务器阻塞;客户端放弃等待后,Redis 上的命令可能仍在执行。
4.6 Go 实现排行榜
type RankItem struct {
UserID string
Score float64
Rank int64 // 对外按 1 开始
}
func AddLeaderboardScore(
parent context.Context,
rdb *redis.Client,
key, userID string,
delta float64,
) (float64, error) {
ctx, cancel := context.WithTimeout(parent, 500*time.Millisecond)
defer cancel()
score, err := rdb.ZIncrBy(ctx, key, delta, userID).Result()
if err != nil {
return 0, fmt.Errorf("increment leaderboard score: %w", err)
}
return score, nil
}
func TopLeaderboard(
parent context.Context,
rdb *redis.Client,
key string,
limit int64,
) ([]RankItem, error) {
if limit <= 0 || limit > 1000 {
return nil, fmt.Errorf("invalid limit: %d", limit)
}
ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
defer cancel()
values, err := rdb.ZRevRangeWithScores(ctx, key, 0, limit-1).Result()
if err != nil {
return nil, fmt.Errorf("query leaderboard: %w", err)
}
result := make([]RankItem, 0, len(values))
for i, value := range values {
userID, ok := value.Member.(string)
if !ok {
return nil, fmt.Errorf("unexpected member type %T", value.Member)
}
result = append(result, RankItem{
UserID: userID,
Score: value.Score,
Rank: int64(i) + 1,
})
}
return result, nil
}
func UserLeaderboardRank(
parent context.Context,
rdb *redis.Client,
key, userID string,
) (rank int64, found bool, err error) {
ctx, cancel := context.WithTimeout(parent, 500*time.Millisecond)
defer cancel()
zeroBased, err := rdb.ZRevRank(ctx, key, userID).Result()
switch {
case err == nil:
return zeroBased + 1, true, nil
case errors.Is(err, redis.Nil):
return 0, false, nil
default:
return 0, false, fmt.Errorf("query user rank: %w", err)
}
}
并发调用 ZINCRBY 时,每条命令在 Redis 端是原子的,不会发生普通的“读旧值、覆盖新值”问题。但业务上的积分变更还应具备事件 ID 或幂等标识,否则客户端超时重试可能把同一积分增加两次。
4.7 Go 实现可靠性更高的延迟领取
错误实现通常是:
ZRANGE 找到到期任务
ZREM 删除任务
执行任务
多个消费者可能同时读到同一个任务。更稳妥的方式是使用短 Lua 脚本,原子地将任务从 ready 集合移动到带租约的 processing 集合。
var claimDueTaskScript = redis.NewScript(`
local ids = redis.call(
'ZRANGE', KEYS[1], '-inf', ARGV[1],
'BYSCORE', 'LIMIT', 0, 1
)
if #ids == 0 then
return nil
end
local id = ids[1]
if redis.call('ZREM', KEYS[1], id) == 1 then
redis.call('ZADD', KEYS[2], ARGV[2], id)
return id
end
return nil
`)
func EnqueueDelayedTask(
parent context.Context,
rdb *redis.Client,
readyKey, taskID string,
runAt time.Time,
) error {
ctx, cancel := context.WithTimeout(parent, 500*time.Millisecond)
defer cancel()
err := rdb.ZAdd(ctx, readyKey, redis.Z{
Score: float64(runAt.UnixMilli()),
Member: taskID,
}).Err()
if err != nil {
return fmt.Errorf("enqueue delayed task: %w", err)
}
return nil
}
func ClaimDueTask(
parent context.Context,
rdb *redis.Client,
readyKey, processingKey string,
now time.Time,
lease time.Duration,
) (taskID string, found bool, err error) {
ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
defer cancel()
result, err := claimDueTaskScript.Run(
ctx,
rdb,
[]string{readyKey, processingKey},
now.UnixMilli(),
now.Add(lease).UnixMilli(),
).Text()
switch {
case err == nil:
return result, true, nil
case errors.Is(err, redis.Nil):
return "", false, nil
default:
return "", false, fmt.Errorf("claim delayed task: %w", err)
}
}
func AckDelayedTask(
parent context.Context,
rdb *redis.Client,
processingKey, taskID string,
) error {
ctx, cancel := context.WithTimeout(parent, 500*time.Millisecond)
defer cancel()
removed, err := rdb.ZRem(ctx, processingKey, taskID).Result()
if err != nil {
return fmt.Errorf("ack delayed task: %w", err)
}
if removed == 0 {
return fmt.Errorf("task %s is no longer in processing set", taskID)
}
return nil
}
这个方案还需要补齐以下机制:
- 租约回收:扫描
processing中score <= 当前时间的任务,将它们重新放回ready。 - 业务幂等:消费者可能处理成功但 ACK 超时,任务随后被重新执行。
- 失败重试:记录重试次数并进行指数退避,超过阈值进入死信区。
- 负载控制:空队列时退避轮询,避免大量消费者持续查询。
- 原子入队:若任务正文和调度索引分别存储,应通过 Lua 或事务共同写入。
- 集群同槽:可使用
delay:{email}:ready与delay:{email}:processing,花括号中的 Hash Tag 保证两个 Key 位于同一 Slot。
Lua 脚本在 Redis 中作为一个原子操作执行,因此脚本必须保持短小;长循环或一次领取大量任务会阻塞其他命令。(Redis)
该方案接近至少一次投递,不能宣称恰好一次。恰好一次通常需要业务数据库的唯一约束、状态机或事务性消息共同参与。
4.8 Go 查询附近位置
func NearbyShops(
parent context.Context,
rdb *redis.Client,
key string,
longitude, latitude, radiusKM float64,
) ([]string, error) {
ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
defer cancel()
shops, err := rdb.GeoSearch(ctx, key, &redis.GeoSearchQuery{
Longitude: longitude,
Latitude: latitude,
Radius: radiusKM,
RadiusUnit: "km",
}).Result()
if err != nil {
return nil, fmt.Errorf("search nearby shops: %w", err)
}
return shops, nil
}
官方文档中的 Go API 同样使用 GeoAdd、GeoLocation 和 GeoSearchQuery。(Redis)
5. 典型业务场景
| 场景 | 适用性 | 数据量要求 | 一致性要求 | 主要性能风险 | 可替代方案 |
|---|---|---|---|---|---|
| 请求 ID 去重 | 适合短周期精确去重 | 中小规模,按时间分桶并设置 TTL | 通常要求较强的原子判重 | Set 无限增长、单 Key 过大 | String SET NX、数据库唯一键、布隆过滤器 |
| 用户标签 | 适合标签成员和反向索引 | 标签分布不能过度倾斜 | 可接受短暂最终一致时容易实现 | 热门标签形成 BigKey/HotKey | 数据库关联表、Search Tag 索引 |
| 权限成员 | 适合角色成员集合 | 权限集合应相对可控 | 高权限场景不能只依赖过期副本读取 | 主从延迟导致撤权不及时 | 关系数据库、策略引擎、本地短缓存 |
| 共同关注 | 集合较小时合适 | 应限制单用户关注上限 | 一般可接受短暂不一致 | 大集合交集阻塞、Cluster 跨槽 | 离线预计算、图数据库、数据库查询 |
| 抽奖候选人 | 适合候选池快照 | 候选集合需可放入内存 | 结果必须可审计 | SPOP 后业务写入失败 | 数据库锁定、预生成随机顺序 |
| 实时排行榜 | 非常适合 | 单榜从几千到数百万需容量评估 | 一般为最终一致;奖励结算需二次校验 | 热榜 Key、深分页、大范围读取 | 数据库窗口函数、离线数仓 |
| 权重任务队列 | 适合简单优先级队列 | 队列长度应受控 | 接受至少一次时较合适 | 饥饿、任务丢失、热点轮询 | Kafka、RabbitMQ、Redis Streams |
| 延迟任务 | 适合中小规模调度 | 应按业务或时间分片 | 需要幂等、租约和回收 | 轮询惊群、超时重试、故障转移 | 专用调度系统、消息队列延迟插件 |
| 滑动时间窗口 | 适合精确事件窗口 | 单用户窗口事件不能过密 | 通常要求原子计数与裁剪 | 每次请求一个 member,内存较大 | 令牌桶、时间分桶计数 |
| 附近的人/门店 | 适合简单圆形或矩形附近查询 | 单 Key 过大时需区域分片 | 多数场景接受最终一致 | 热区域、返回结果过多、坐标错误 | Redis Search、PostGIS、专业 GIS |
6. 底层实现
6.1 Set 的内部编码
经典面试资料常把 Set 概括为:
- 全整数、小集合:
intset。 - 其他情况:
hashtable。
这一回答对不少旧版本和经典实现是有意义的,但对当前版本不够完整。Redis 8.8 的 Set 源码包含:
intsetlistpackhashtable
三类编码分支。(GitHub)
intset
intset 是紧凑的有序整数数组:
- 只适合能够解析为整数的成员。
- 根据整数范围选择较小的整数宽度。
- 通过连续内存降低指针和哈希桶开销。
- 查找可利用有序数组,但插入中间位置可能需要移动数据。
它适合小规模整数集合,核心取舍是:用较高的局部操作成本换取较低内存占用和更好的缓存局部性。
listpack
listpack 是紧凑连续编码:
- 适合成员较少、成员长度较短的 Set。
- 减少哈希表桶、指针和独立内存分配的开销。
- 查找通常需要线性扫描,但因为集合很小且内存连续,实际可能较快。
- 随着成员增加或成员过长,会转换成更适合大集合的编码。
当前配置中可通过 set-max-listpack-entries 和 set-max-listpack-value 控制相关阈值;具体默认值可能随版本和发行配置变化,不能把阈值写死为业务假设。(GitHub)
hashtable
大集合通常采用哈希表:
SISMEMBER平均接近O(1)。- 增删成员平均接近
O(1)。 - 内存成本高于连续紧凑结构。
- 扩容和渐进式 rehash 会带来额外工作。
- 随机访问能力强,适合大规模成员集合。
官方命令复杂度把 SADD 表述为每个元素 O(1),这是面向命令使用者的典型复杂度;分析具体编码时,还应考虑紧凑编码的小规模线性扫描和转换成本。(Redis)
6.2 Set 集合运算算法
SINTER
Redis通常优先从较小的集合出发,逐个检查其成员是否存在于其他集合中。这样比遍历最大集合更经济。
假设三个集合大小分别为:
A = 100
B = 100 万
C = 500 万
从 A 出发最多只需验证 100 个候选,而不是先遍历 C。但每个候选仍要在其他集合中执行成员检查,集合数量增加也会增加成本。
SUNION
遍历所有输入集合,将成员加入结果集合并自然去重。成本与输入成员总量相关,不仅与最终结果数量相关。
SDIFF
差集对参数顺序敏感:
SDIFF A B C
表示 A - B - C。其成本由第一个集合和后续集合的规模共同决定。因此,不能随意交换 Key 顺序来“优化”而不改变业务语义。
STORE 版本
SINTERSTORE、SUNIONSTORE、SDIFFSTORE 把结果存入 Redis,能减少大结果向客户端传输,但不会消除服务器端计算成本,而且会增加写入、内存和复制流量。
只关心交集数量时,应优先考虑 SINTERCARD,避免传回完整成员列表。
6.3 Sorted Set 的紧凑编码与双索引
小型 Sorted Set 可以使用 listpack,降低内存开销;集合扩大后,经典的大集合实现同时维护:
- 字典
- 跳表
官方 Sorted Set 文档明确描述了其跳表与哈希表组合结构。(Redis)
字典负责什么
字典通常以 member 为 Key,快速定位其 score 或关联节点:
- 查询成员是否存在:平均
O(1)。 - 获取成员当前 score:平均
O(1)。 - 更新已存在成员前,快速找到旧值。
若只有跳表,根据 member 查找会很困难,因为跳表主要按 (score, member) 排序,调用方只提供 member 时无法高效定位。
跳表负责什么
跳表按照 (score, member) 有序组织节点:
- 插入、删除:期望
O(log N)。 - 按 score 查询范围:定位起点后顺序遍历。
- 获取排名:通过层级跨度信息计算。
- Top N:从高分端反向遍历。
- 同分成员:再按 member 二进制顺序排列。
为什么不是只用平衡树
平衡树同样可以实现 O(log N),Redis 选择跳表主要因为:
- 算法和实现相对直接。
- 范围遍历自然。
- 通过随机层级维持期望复杂度。
- 插入和删除涉及的结构调整相对局部。
- 可以方便维护前后向链表和排名跨度。
这不是“跳表在所有方面都优于红黑树”,而是 Redis 在复杂度、范围查询、实现维护和工程成本之间的选择。
为什么不能只用字典
字典能快速定位 member,却无法高效回答:
- 分数最高的前 100 名是谁?
- score 在
[1000, 2000]之间的成员有哪些? - 某个成员排第几?
- 删除最小 score 的任务是谁?
因此,字典和跳表是两套不同访问路径的组合,代价是更多内存和每次写入同时更新两个索引。
6.4 score 与排序键
Sorted Set 实际按以下组合键排序:
(score, member)
因此:
- member 唯一。
- score 可以重复。
- score 相同时 member 决定顺序。
- 修改 member 需要删除旧成员再添加新成员。
- 修改 score 会改变其在跳表中的位置。
实现“同分时先到先得”可考虑:
- 保留 score 为业务积分,在 member 前附加可排序序号。
- 把业务积分和序号编码为不超过
2^53的复合整数。 - 将同分用户放入另一个按时间排序的结构。
- 结算时在数据库中使用明确的第二排序字段。
第一种方式会影响以真实用户 ID 查询,需要维护映射;第二种方式需要严格证明数值范围;第三种方式增加一致性成本。
6.5 GEO 的存储模型
Redis GEO 将经纬度编码为大约 52 位的整数值,并作为 Sorted Set 的 score 存储。经纬度的二进制位交错组合,使空间上相近的位置通常具有相近的编码。(Redis)
查询附近成员时,大致过程为:
- 根据中心点和半径计算覆盖区域。
- 找到中心网格及相邻网格。
- 利用 Sorted Set 的 score 范围筛选候选点。
- 对候选点计算实际球面距离。
- 过滤掉落在边界框内但不在目标圆形内的点。
- 根据需要排序、限制数量并返回。
GEOADD 要求经度在 [-180, 180],纬度在约 [-85.05112878, 85.05112878] 范围内。Redis 的距离模型把地球近似为球体,边缘情况下误差可能达到约 0.5%,因此不适合测绘、计费里程或法律边界判定。(Redis)
6.6 主要命令复杂度
| 命令 | 典型复杂度 | 关键风险 |
|---|---|---|
SADD / SREM | 每成员平均 O(1) | 批量参数过多、编码转换 |
SISMEMBER | 平均 O(1) | 超大热 Key |
SINTER | 最坏约 O(N × M) | 大集合阻塞 |
SUNION / SDIFF | 约 O(输入成员总量) | 大结果与网络开销 |
ZADD / ZINCRBY | 每成员 O(log N) | 热榜写竞争 |
ZRANGE | O(log N + M) | 大范围、深分页 |
ZRANK / ZREVRANK | O(log N) | 高频单榜查询 |
ZPOPMIN | 多成员约 O(M log N) | 弹出后任务处理失败 |
GEOADD | O(log N) | 热区域频繁更新 |
GEODIST | O(1) | 模型精度边界 |
GEOSEARCH | 约 O(N + log M) | 查询区域大、候选点多 |
其中 N、M 在不同命令文档中含义不同,不能脱离命令定义机械比较。(Redis)
7. 高性能、高并发、高可用分析
7.1 高性能
CPU
Set 单成员操作和 Sorted Set 单点操作通常很快,但以下命令可能消耗大量 CPU:
- 多个大 Set 的交并差。
- 返回大范围的
ZRANGE。 - 过大区域的
GEOSEARCH。 - 大批量
ZREM、ZPOPMIN。 - 包含循环的长 Lua 脚本。
客户端超时只能限制调用方等待时间,不能保证已经发送到 Redis 的命令立即停止。
内存
主要内存风险包括:
- Set 或 Sorted Set 成为 BigKey。
- Sorted Set 同时维护字典和跳表,元数据成本较高。
- 时间窗口为每个请求创建唯一 member。
- 延迟任务长期堆积。
- GEO 在一个城市级 Key 中存放过多对象。
SUNION等命令临时创建巨大结果。
网络
返回十万个成员时,即使服务器计算很快,网络传输和客户端反序列化也可能成为主要延迟来源。
常见控制方式:
- 设置分页和最大返回条数。
- 只需要数量时使用计数命令。
- 使用
STORE版本在 Redis 内保存中间结果,但要评估额外内存。 - 不向普通接口直接暴露“返回全部成员”能力。
Pipeline
Pipeline 能减少多条独立命令的网络往返,但不会降低单条 SINTER 或 GEOSEARCH 的算法复杂度,也不会使多条命令自动具备原子性。(Redis)
7.2 高并发
原子性
单条 SADD、ZINCRBY、ZPOPMIN 在单个 Redis 节点上是原子的,但业务流程通常不是单条命令:
领取任务 → 执行业务 → 写数据库 → ACK
这整个流程不会因为使用 Redis 就自动原子化。
热点
以下 Key 容易成为 HotKey:
- 全服总榜。
- 超级用户的关注集合。
- 全国门店 GEO Key。
- 所有延迟任务共用一个 ZSet。
- 热门标签的成员集合。
可选的分散方式包括:
- 按赛季、地区、租户或业务类型分 Key。
- 维护分区榜和异步总榜。
- GEO 按区域网格分片。
- 延迟任务按业务、时间桶或分区号拆分。
- 限制单集合成员规模。
但分片后,跨分片合并、全局排名和跨区域查询会变复杂。
惊群与轮询
几十个消费者同时每 10 毫秒查询延迟 ZSet,会在空队列和任务到期瞬间制造突发流量。应使用:
- 随机抖动。
- 空队列指数退避。
- 每次限制领取数量。
- 消费者并发上限。
- 通知与轮询结合,但通知不能作为唯一可靠信号。
重试
客户端超时时,命令可能:
- 根本没到服务器。
- 已到服务器但尚未执行。
- 已执行,只是响应丢失。
因此对 ZINCRBY、任务领取、ACK 等写操作盲目重试可能产生重复副作用。
7.3 高可用
Redis 主从复制默认是异步的。主节点刚确认的写入可能还未到达副本;此时发生故障转移,最近的 Set、排行积分或任务状态可能丢失。WAIT 可以降低概率,但不能把 Redis 变成强一致 CP 系统。(Redis)
需要特别考虑:
- 从副本读排行榜可能看到旧名次。
- 撤销权限后,从副本读可能短时间仍显示有权限。
- 任务刚从
ready移到processing时发生切换,可能重新出现或消失。 - 消费者收到超时后不能确定领取是否成功。
- 故障恢复后租约回收可能造成重复执行。
因此:
- 权限等高风险判断应谨慎使用副本读。
- 延迟任务处理器必须幂等。
- 关键结算应回到数据库或权威账本复核。
- 持久化、复制和业务幂等应共同设计,不能把“部署了副本”理解为“不会丢数据”。
Redis Cluster 中,多 Key 命令和 Lua 脚本通常要求相关 Key 位于同一 Hash Slot,应在建模阶段使用 Hash Tag,而不是出现 CROSSSLOT 后临时修改。(Redis)
8. 常见错误与生产事故
8.1 在核心接口中对百万级集合执行 SINTER
- 现象:Redis 延迟突增,其他 GET、SET 也变慢,慢日志出现集合命令。
- 根因:共同关注接口直接对多个大集合求交;结果虽少,候选扫描量很大。
- 排查方法:检查
SLOWLOG、集合基数、命令耗时、CPU 和客户端输出大小。 - 修复方案:限制关系数量,改为离线预计算,或从最小候选集分批在应用侧验证。
- 预防措施:为集合大小设置监控阈值,对复杂度高的命令制定准入规则。
8.2 排行榜同分顺序与产品规则不一致
- 现象:相同积分用户的排名看似随机,或正序、倒序结果相反。
- 根因:产品要求“先达到该分数者优先”,代码却依赖 member 字典序。
- 排查方法:检查重复 score、member 格式及查询是否使用
REV。 - 修复方案:增加明确的第二排序设计,或在结算数据库中二次排序。
- 预防措施:排行榜需求评审时明确同分规则、并列名次规则和分页规则。
8.3 毫秒、秒或纳秒时间戳混用
- 现象:延迟任务提前几十年执行、永不到期,或时间窗口完全失效。
- 根因:生产者使用秒,消费者按毫秒比较;或将纳秒时间戳直接转为
float64。 - 排查方法:输出 score,检查数量级;对照
time.Now().Unix()、UnixMilli()。 - 修复方案:统一使用 Unix 毫秒,并把单位写入变量名和接口文档。
- 预防措施:增加时间范围校验,例如拒绝超过合理未来窗口的执行时间。
8.4 使用 ZRANGE 后 ZREM 领取任务
- 现象:同一任务被多个 worker 同时执行。
- 根因:查询和删除不是一个原子操作。
- 排查方法:对照多个消费者日志,检查是否读取了同一个 taskID。
- 修复方案:使用 Lua 原子地从 ready 移到 processing。
- 预防措施:为任务执行建立唯一业务键和幂等状态机。
8.5 直接用 ZPOPMIN 实现延迟任务
- 现象:未来任务被提前取出;worker 失败后任务永久消失。
- 根因:
ZPOPMIN只关心最小 score,不判断它是否已经到期,而且弹出即删除。 - 排查方法:比较弹出 score 与当前时间,检查失败任务是否存在 processing 记录。
- 修复方案:先按 score 过滤到期任务,再原子转移到 processing,并设置租约。
- 预防措施:区分“优先级队列”和“按时间到期的调度队列”。
8.6 GEO 经纬度写反
- 现象:成员出现在其他国家,查询不到附近门店,或
GEOADD报坐标越界。 - 根因:业务对象通常写成纬度、经度,而 Redis 参数顺序是经度、纬度。
- 排查方法:使用
GEOPOS检查实际坐标,验证经度绝对值和纬度范围。 - 修复方案:定义明确的
Longitude、Latitude字段,禁止使用模糊的x、y。 - 预防措施:增加城市边界校验和固定坐标测试用例。
8.7 Redis Cluster 出现 CROSSSLOT
- 现象:单 Key 命令正常,
SINTER或延迟任务 Lua 脚本报跨 Slot 错误。 - 根因:相关 Key 没有使用相同 Hash Tag。
- 排查方法:使用
CLUSTER KEYSLOT检查 Key 的 Slot。 - 修复方案:设计为
follow:{user-group}:a、follow:{user-group}:b,或在应用层完成跨分片合并。 - 预防措施:在进入 Cluster 前完成多 Key 访问模式审计。
8.8 故障转移后任务重复执行
- 现象:Redis 主从切换后,少量订单通知或定时任务重复处理。
- 根因:异步复制、领取响应丢失、processing 状态回退或租约重放。
- 排查方法:对照故障时间、复制偏移、任务状态及业务幂等记录。
- 修复方案:业务数据库增加唯一执行记录;重复请求返回已有结果。
- 预防措施:以“可能重复”为正常故障模型进行演练,而不是依赖恰好一次假设。
9. 方案选型与权衡
| 需求 | Set | Sorted Set | GEO | 数据库/专业系统 |
|---|---|---|---|---|
| 精确去重 | 优 | 可以但浪费 score | 不适合 | 唯一索引更持久 |
| 成员关系判断 | 优 | 可用但成本较高 | 不适合 | 复杂关系可用图数据库 |
| 交并差 | 原生支持 | 支持部分聚合命令 | 不适合 | SQL 灵活但延迟较高 |
| 排名与范围 | 不支持顺序 | 优 | 仅地理范围 | 数据库适合离线结算 |
| 优先级队列 | 不适合 | 适合简单模型 | 不适合 | 专用 MQ 可靠性机制更完整 |
| 延迟任务 | 不适合 | 可实现,但需补可靠性 | 不适合 | 专用调度系统更成熟 |
| 附近位置 | 不适合 | 可自行编码但复杂 | 优于简单附近查询 | PostGIS/Redis Search 更强 |
| 强一致结算 | 不适合作为唯一账本 | 不适合作为唯一账本 | 不适合 | 事务数据库 |
| 超大集合分析 | 在线风险较高 | 在线风险较高 | 区域过大风险较高 | 数仓、搜索或离线计算 |
选型原则不是“Redis 能实现就使用 Redis”,而是先确定:
- 数据是否必须精确和持久。
- 单 Key 最大规模。
- 是否需要跨 Key 运算。
- 是否允许最终一致。
- 是否接受重复执行。
- 故障后如何恢复和审计。
- Redis 是权威数据源还是可重建索引。
10. 高频面试题
10.1 Set 适合解决什么问题?
问题
Redis Set 的核心特点和典型场景是什么?
推荐回答
简洁回答:Set 是无序且成员唯一的字符串集合,适合去重、成员判断、标签、权限和集合关系运算。
详细展开:SADD 自动去重,SISMEMBER 适合成员判断,SINTER、SUNION、SDIFF 分别实现交集、并集和差集。它不保证插入顺序,因此不应实现时间线或有序队列。(Redis)
面试官追问
- Set 和 Hash 有什么区别?
- 抽奖应使用
SRANDMEMBER还是SPOP? - Set 能否按插入顺序返回?
常见错误回答
“Set 就是 Go 的 map。”该回答忽略了集合运算、Redis 网络模型和内部编码差异。
评分点
- 初级:知道去重和无序。
- 中级:能说出交并差和业务建模。
- 高级:能说明 BigKey、跨 Slot 和一致性风险。
10.2 如何实现共同好友或共同关注?
问题
两个用户的共同关注怎么实现?
推荐回答
简洁回答:为每个用户维护关注 Set,用 SINTER 求交集;但大集合不能无条件在线求交。
详细展开:follow:userA 和 follow:userB 分别存放被关注用户 ID。集合较小时可直接 SINTER;只需要数量时可考虑 SINTERCARD。大规模社交关系应限制集合规模、预计算共同关系,或从较小集合取候选后分批判断。
面试官追问
- 两个集合都达到千万级怎么办?
- Redis Cluster 下能直接求交吗?
- 如何只返回前 20 个共同关注?
常见错误回答
“SINTER 是 Redis 原生命令,所以一定很快。”
评分点
- 初级:能给出
SINTER。 - 中级:知道复杂度和大结果风险。
- 高级:能提出预计算、分片、候选裁剪和 Cluster 同槽方案。
10.3 为什么 O(N) 命令也可能造成严重阻塞?
问题
SUNION 是 O(N),为什么仍然危险?
推荐回答
简洁回答:O(N) 只说明增长趋势,不说明 N 的大小,也不包含结果分配、协议编码、网络发送和客户端解析成本。
详细展开:百万级输入下,线性扫描本身就很重;Redis 还要构造结果并发送。命令执行占用时间变长时,同一节点上其他命令也会排队,因此一个低频慢命令可能拖慢大量高频 GET。
面试官追问
- 客户端设置 50 毫秒超时能否解决?
- 使用 Pipeline 是否会更快?
SUNIONSTORE有何不同?
常见错误回答
“O(N) 已经是最优复杂度,所以没有优化空间。”
评分点
- 初级:知道 N 大会慢。
- 中级:能分析 CPU、内存和网络。
- 高级:能说明服务端继续执行、输出缓冲区和隔离策略。
10.4 Set 的底层只有 intset 和 hashtable 吗?
问题
Redis Set 的底层编码有哪些?
推荐回答
简洁回答:旧资料常概括为 intset 和 hashtable;当前 Redis 8.8 还包含 listpack Set 编码。
详细展开:整数小集合可用 intset;成员少且短时可使用 listpack;规模增长或不再满足紧凑条件时使用哈希表。紧凑结构节省内存并改善局部性,哈希表则提供更好的大规模随机访问。(GitHub)
面试官追问
- 为什么紧凑编码可能比哈希表更快?
- intset 插入为什么可能不是 O(1)?
- 如何查看对象编码?
常见错误回答
“所有 Redis 版本都只有 intset 和 hashtable。”
评分点
- 初级:知道经典两种编码。
- 中级:能解释转换条件和内存取舍。
- 高级:能主动指出版本差异,不机械背旧结论。
10.5 Sorted Set 如何处理重复 member 和重复 score?
问题
ZSet 能不能有重复分数和重复成员?
推荐回答
简洁回答:member 必须唯一,score 可以重复;重复添加同一 member 默认更新 score。
详细展开:Redis 先按 score 排序,相同 score 再按 member 的二进制字典序排序;反向查询时,同分成员使用反向字典序。因此相同积分不等于并列顺序随机,但也不等于按到达时间排序。(Redis)
面试官追问
- 同分时先到先得怎么实现?
ZADD NX和XX有什么作用?- 修改 score 后排名如何变化?
常见错误回答
“score 也必须唯一。”
评分点
- 初级:知道 member 唯一。
- 中级:知道同分字典序。
- 高级:能设计第二排序字段并分析精度与查询代价。
10.6 Sorted Set 为什么同时使用字典和跳表?
问题
只用跳表或只用字典不行吗?
推荐回答
简洁回答:字典负责按 member 快速定位,跳表负责按 score 排序、范围查询和排名,两者优化的是不同访问路径。
详细展开:只用字典无法高效获取 Top N 和 score 范围;只用跳表时,仅根据 member 查找旧 score 不够高效。双索引让查询和排序都快,但增加了内存,并要求每次写入同步维护两套结构。(Redis)
面试官追问
ZADD为什么是O(log N)?ZSCORE为什么通常很快?- 双索引会不会数据不一致?
常见错误回答
“字典用于去重,跳表只是为了好看地遍历。”
评分点
- 初级:能说出字典加跳表。
- 中级:能对应具体命令。
- 高级:能说明内存代价、写放大和一致维护。
10.7 为什么 Redis 使用跳表而不是红黑树?
问题
跳表相对平衡树有什么工程优势?
推荐回答
简洁回答:两者都能达到对数级查找,跳表实现更直接,范围遍历和节点前后移动自然,适合 Redis 的排名和范围访问。
详细展开:跳表通过随机层级获得期望 O(log N),无需复杂旋转;底层链表便于范围扫描。它并非理论上全面优于红黑树,而是更符合 Redis 的实现目标。
面试官追问
- 跳表最坏复杂度是什么?
- 随机层级如何影响内存?
- 排名如何做到
O(log N)?
常见错误回答
“跳表永远比红黑树快。”
评分点
- 初级:知道二者都可排序。
- 中级:知道随机层级和范围遍历。
- 高级:能从实现复杂度、内存和工作负载说明权衡。
10.8 score 有什么精度问题?
问题
为什么不能直接把纳秒时间戳作为 ZSet score?
推荐回答
简洁回答:score 是 double,只能精确表示 [-2^53, 2^53] 范围内的整数;Unix 纳秒时间戳已超过这个范围。
详细展开:超过精确整数边界后,两个不同整数可能变成相同的浮点表示,导致排序和范围边界异常。毫秒时间戳通常安全;微秒接近但仍在范围内;复合 score 必须计算最大值。(Redis)
面试官追问
- 金额能否直接作为 score?
- 如何实现时间加序号?
- 小数 score 是否完全不能使用?
常见错误回答
“double 有 64 位,所以所有 int64 都能精确表示。”
评分点
- 初级:知道浮点有精度问题。
- 中级:记得
2^53边界。 - 高级:能结合时间单位、金额缩放和复合编码给出方案。
10.9 如何设计一个实时排行榜?
问题
如何支持加分、Top 100、个人排名和赛季结算?
推荐回答
简洁回答:按赛季建立 ZSet,以 userID 为 member、积分为 score,使用 ZINCRBY、ZRANGE REV 和 ZREVRANK;结算时回数据库复核。
详细展开:Key 应包含游戏、区服和赛季;限制 Top N 查询范围;定义同分规则;为积分事件设置幂等 ID;大规模全服榜可采用分区榜加异步总榜。奖励结算不应只以 Redis 当前值作为唯一账本。
面试官追问
- 如何防止重复加分?
- 如何处理十亿用户?
- 如何实现用户附近排名?
常见错误回答
“所有用户放在一个 ZSet 就完成了。”
评分点
- 初级:掌握核心命令。
- 中级:能设计 Key、同分和分页。
- 高级:能讨论热 Key、分片、结算一致性与容灾。
10.10 如何用 Sorted Set 实现延迟任务?
问题
最小可用方案是什么,生产方案还缺什么?
推荐回答
简洁回答:以执行时间为 score、taskID 为 member;消费者原子地把到期任务从 ready 移到 processing,再执行和 ACK。
详细展开:不能直接 ZRANGE 后 ZREM,也不能无条件 ZPOPMIN。生产方案要有领取租约、超时回收、重试次数、死信、幂等、空队列退避和故障恢复。Redis 适合简单调度,但可靠性语义需要业务自行补齐。
面试官追问
- worker 处理成功但 ACK 失败怎么办?
- 如何避免轮询惊群?
- Redis 切主时可能发生什么?
常见错误回答
“ZPOPMIN 是原子的,所以能够保证恰好一次。”
评分点
- 初级:知道时间戳 score。
- 中级:知道查询删除竞态。
- 高级:能完整描述租约、回收、幂等和故障模型。
10.11 ZPOPMIN 原子,为什么仍不能保证任务恰好一次?
问题
单命令原子性和业务恰好一次有什么区别?
推荐回答
简洁回答:ZPOPMIN 只保证弹出动作不会被两个客户端同时成功执行,不保证弹出后的业务处理和 ACK 与 Redis 原子提交。
详细展开:弹出后 worker 可能宕机,任务会丢;业务处理成功后响应丢失,客户端可能重试;故障转移也可能使状态回退。恰好一次通常要依赖业务唯一约束、幂等状态机或事务边界。
面试官追问
- 至少一次如何实现?
- 至多一次如何实现?
- 什么场景可以接受
ZPOPMIN?
常见错误回答
“Redis 命令是原子的,所以整个消费流程也是原子的。”
评分点
- 初级:区分弹出和处理。
- 中级:能解释丢失与重复。
- 高级:能比较至少一次、至多一次和事实上的恰好一次。
10.12 Redis GEO 的底层是什么?
问题
GEO 是否是一种独立于 Sorted Set 的数据结构?
推荐回答
简洁回答:GEO 的底层存储是 Sorted Set,经纬度编码后写入 score,member 是位置对象 ID。
详细展开:Redis 先用空间编码缩小候选范围,再对候选点计算球面距离。它适合简单的圆形和矩形附近查询,但不支持复杂 GIS 查询;删除 GEO 成员可以使用 ZREM。(Redis)
面试官追问
- 为什么要检查相邻网格?
- Redis 计算的是道路距离吗?
- GEO 查询误差来自哪里?
常见错误回答
“Redis GEO 使用二维 R 树,并能进行精确导航距离计算。”
评分点
- 初级:知道能查询附近位置。
- 中级:知道底层是 ZSet 和空间编码。
- 高级:能说明候选过滤、球面模型、误差和专业 GIS 边界。
10.13 Go 客户端调用这些结构时要注意什么?
问题
go-redis/v9 在并发、超时和错误处理方面有哪些要点?
推荐回答
简洁回答:Client 应全局复用并允许多 goroutine 并发使用;每次调用传 Context 和超时;区分 redis.Nil、超时和真正服务端异常。
详细展开:不要每个请求创建连接;对 ZREVRANK 等“成员不存在”结果使用 errors.Is(err, redis.Nil);对空集合结果不要误判为异常;写命令超时后结果可能未知,重试前必须确认幂等。Cluster 多 Key 操作还要保证同槽。
面试官追问
- Context 超时后 Redis 命令一定停止吗?
- 哪些命令不存在成员时返回
redis.Nil? - Pipeline 是否线程安全?
常见错误回答
“任何非 nil error 都直接重试三次。”
评分点
- 初级:会创建 Client 和调用命令。
- 中级:能正确处理 Context 与
redis.Nil。 - 高级:能分析未知执行结果、幂等重试、连接池和 Cluster 路由。
11. 一分钟面试回答
Set 是无序且成员唯一的集合,适合去重、标签、权限和共同关系,常用命令包括 SADD、SISMEMBER、SINTER、SUNION 和 SDIFF。但交并差会扫描大量成员并生成结果,大集合上可能阻塞 Redis,不能因为是原生命令就默认安全。Sorted Set 在唯一 member 上增加 double score,底层大集合同时维护字典和跳表:字典用于按 member 快速定位,跳表用于排序、范围和排名。相同 score 按 member 的二进制字典序排列,score 精确整数范围只有正负 2^53,所以纳秒时间戳和未经证明的复合分值有风险。排行榜可用 ZINCRBY、ZRANGE REV 和 ZREVRANK;延迟任务以执行时间为 score,但必须用 Lua 原子领取、processing 租约、超时回收和业务幂等,不能宣称恰好一次。GEO 底层也是 Sorted Set,适合简单附近查询,不替代 PostGIS 或完整 GIS。生产上还要考虑 HotKey、BigKey、Cluster 同槽、异步复制和故障转移后的重复或数据丢失。
12. 本章总结
- Set 的核心价值是唯一成员、快速成员判断和集合运算,而不是有序遍历。
- 集合运算的风险由输入规模、集合数量、结果大小和网络共同决定。
- Sorted Set 的排序键实际是
(score, member),同分业务规则必须明确设计。 - 字典与跳表分别服务于按成员访问和按顺序访问,双索引以更多内存换取多种高效查询。
- double score 不能精确表示所有 int64,时间单位和复合评分必须经过数值边界分析。
- Sorted Set 可以实现延迟任务,但调度索引不等于可靠消息系统。
- GEO 适合简单附近查询,不能承担道路距离、复杂多边形和高精度测绘。
- Redis 的单命令原子性不能扩展成整个业务流程的恰好一次或强一致保证。
13. 自测清单
- Set 为什么不能用于需要稳定插入顺序的业务?
- 两个集合的交集结果很少,为什么
SINTER仍可能很慢? SUNIONSTORE相比SUNION解决了什么,又增加了什么成本?- 当前 Redis 8.8 的 Set 可能采用哪些内部编码?
- Sorted Set 中两个成员 score 相同,最终顺序由什么决定?
- 为什么 double 不能精确表示全部 int64?
- 字典和跳表分别为 Sorted Set 的哪些命令提供支持?
- 为什么
ZRANGE加ZREM不能安全地领取延迟任务? - 延迟任务的 processing 租约解决了什么问题,仍然无法解决什么问题?
- Redis GEO 与 PostGIS 或 Redis Search 的适用边界有什么不同?
14. 官方资料
- Redis Open Source 下载与当前版本信息。(Redis)
- Redis Set 数据类型文档。(Redis)
SADD、SINTER、SUNION、SDIFF命令文档。(Redis)- Redis Sorted Set 数据类型文档。(Redis)
ZADD、ZRANGE、ZRANK、ZPOPMIN命令文档。(Redis)- Redis GEO 数据类型及
GEOADD、GEOSEARCH、GEODIST文档。(Redis) - Redis 8.8
t_set.c与t_zset.c官方源码。(GitHub) - Redis Lua、Pipeline、复制和 Cluster 规范。(Redis)